import os
import glob
import random

import libtcodpy as libtcod

import console
import menu
import splash
import objects
import lights
import music
import effects
import results

import actions
import utils

DEBUG = False

LOG_LENGTH = 4
CHOMP_UPDATE = 100
CHOMP_SPLATTER_RADIUS = 3
MAX_NOTES = 13
CHOMPER_LEVEL_MAP = (5, 7, 9, 13)

MIN_ENERGY = 0.3

class Stage(object):
    def __init__(self, raw_width, height, level=1):
        chomper_count = CHOMPER_LEVEL_MAP[level-1]
        self.height = height
        self.width = 2 * chomper_count + 3
        self.x, self.y = (raw_width - self.width) / 2, 0

        self.con = console.Console(self.width, self.height)

        # Only use three tiles on this 'map'
        bright = libtcod.Color(100, 90, 25)
        wall = utils.Tile(libtcod.Color(130, 110, 50), libtcod.Color(0, 0, 100), False, False)
        glass = utils.Tile(bright, libtcod.Color(50, 50, 150), False, True, 
                           libtcod.CHAR_HLINE, libtcod.grey)
        floor = utils.Tile(bright, libtcod.Color(50, 50, 150), True, True)

        self.map = []
        self.lights = []
        self.chompers = []
        self.spawners = []

        # Draw the border and floor tiles
        for y in xrange(self.height):
            row = []
            edge = y == self.height - 1
            for x in xrange(self.width):
                if (x in (0, self.width - 1)) or edge:
                    row.append(wall)
                else:
                    row.append(floor)
            self.map.append(row)

        # Draw glass and lights
        y = 3
        for x in xrange(1, self.width - 1):
            self.map[y][x] = glass

        y = self.height - 2
        for x in (1, self.width / 2, self.width - 2):
            light = utils.Light(bright, 8, x, y, char=None, name='light', passable=True)
            self.map[y][x] = light
            self.lights.append(light)

        # Add the chompers
        y = self.height - 2
        self.chompers = [objects.Chomper(x, y) for x in xrange(1, self.width - 1)]
        for ix, chomper in enumerate(self.chompers):
            splatter = slice(max(0, ix-CHOMP_SPLATTER_RADIUS), 
                             min(len(self.chompers)-1, ix+CHOMP_SPLATTER_RADIUS))
            for other in self.chompers[splatter]:
                chomper.splatter.append(other)

        # Add the spawners
        x, y = range(2, self.width-2, 2), 0
        map_colors = [libtcod.Color(255, 0, 0), libtcod.Color(0, 255, 0), libtcod.Color(90, 90, 255)]
        map_ix = [0, len(x)/2, len(x)-1]
        colors = libtcod.color_gen_map(map_colors, map_ix)

        self.spawners = [objects.Spawner(x[ix], y, colors[ix], level) for ix in range(len(x))]

        # Other setup
        x, y = self.width / 2, self.height - 4
        self.map[y][x] = utils.Tile(bright, libtcod.Color(50, 50, 150), True, True, name='entry')
        self.monsters = dict()

    def draw(self):
        for y, row in enumerate(self.map):
            for x, tile in enumerate(row):
                char = ' ' if tile.char is None else tile.char
                self.con.putchar_ex(x, y, char, tile.color, tile.dark)

        for item in self.spawners + self.chompers:
            self.put(item)

    def erase(self, item):
        x, y = item.x, item.y
        tile = self.map[y][x]
        
        char = ' ' if not tile.char else tile.char
        self.con.putchar_ex(x, y, char, tile.color, tile.dark)

    def put(self, item):
        self.con.putchar(item.x, item.y, item.char, libtcod.BKGND_NONE)
        self.con.set_fore(item.x, item.y, item.color)

    def update(self, root):
        self.con.blit(0, 0, self.con.width, self.con.height, root, self.x, self.y)

class Message(object):
    def __init__(self, msg):
        self.message = msg
        self.count = 1

    def __str__(self):
        if self.count == 1:
            return self.message
        else:
            return "%s (x %d)" % (self.message, self.count)

    def __repr__(self):
        return "Message(%r, %d)" % (self.message, self.count)

class Status(object):
    def __init__(self, x, y, width, height, alpha=1.0):
        self.x = x
        self.y = y
        self._con = console.Console(width, height)
        self.alpha = alpha

        self.clear()

    def clear(self):
        self.log = []
        self.last = None
        self._con.clear()

    def intro(self, player, song, level):
        self.clear()
        self.add("And now, for one night only... %s!" % player.name)
        self.add("Song: %r [Rank: %d] (Level: %d)" % (song.name, song.difficulty, level))
        self.add("Get ready...")

    def add(self, msg, unique=False):
        if (not self.log) or (msg != self.log[-1].message) or unique:
            self.log.append(Message(msg))
        else:
            self.log[-1].count += 1

    def update(self, root, player, score, energy):
        self._con.clear()

        self._con.foreground = libtcod.white
        self._con.hline(0, 0, self._con.width, libtcod.BKGND_SET)
        self._con.print_left(1, 0, libtcod.BKGND_NONE, "HP:%d,AC:%d" % (player.hp, player.ac))
        self._con.print_right(23, 0, libtcod.BKGND_NONE, " %.01f Energy" % energy)
        self._con.print_left(55, 0, libtcod.BKGND_NONE, "Score: %d " % score)
        self._con.print_right(78, 0, libtcod.BKGND_NONE, "%+d/%s" % (player.to_hit.plus, str(player.damage)))

        self._con.foreground = libtcod.light_grey
        text = '\n'.join([str(m) for m in self.log[-LOG_LENGTH:]])
        self._con.print_left_rect(1, 1, self._con.width - 3, LOG_LENGTH, libtcod.BKGND_NONE, text)

        self._con.blit(0, 0, self._con.width, self._con.height, root, self.x, self.y, background_alpha=self.alpha)

class Game(object):
    def __init__(self, root, exit_offset, jukebox, log, song_path='songs'):
        self.root = root
        self.exit_offset = exit_offset
        self.jukebox = jukebox

        log('Loading music.')
        files = glob.glob(os.path.join(song_path, '*.dat'))
        self.songs = [music.Song(file) for file in files]
        self.songs.sort()

        self._bak = console.Console(root.width, root.height)

        self.stage_limit = self.root.height - LOG_LENGTH
        self.status = Status(0, self.stage_limit - 1, self.root.width, LOG_LENGTH + 1, 0.66)
        self.effects = effects.Effects(self.root, log)

        self.active = False
        self.moves = dict()

        self.score = 0   # Points
        self.energy = 0  # Excitment

        self.chomp_update = 0
        self.spawn_update = 100

    def play(self, _unused_root, player):
        self.jukebox.event('song_begin')
        self.song, self.level = get_song(self.root, self.songs)

        if self.song is None:
            player.x += self.exit_offset[0]
            player.y += self.exit_offset[1]
            return
        self.song.reset()

        # Since the stage width now varies, the stage has to be rebuilt at entry.
        self.stage = Stage(self.root.width, self.stage_limit, self.level)
        self.fov = lights.FOV(self.stage.map, self.stage.lights)  # stage.lights >> heh.
        self.static_light = self.fov.static_map
        self.fov_update = True

        self.chomper_y = self.stage.height - 2

        self.player = player

        self.root.blit(0, 0, self.root.width, self.root.height, self._bak, 0, 0)
        exit_x, exit_y = player.x + self.exit_offset[0], player.y + self.exit_offset[1]
        player.x, player.y = utils.find_tile(self.stage.map, 'entry')

        self.stage.monsters = dict()
        for chomper in self.stage.chompers:
            #chomper.blood = 1
            chomper.clean()
        self.status.intro(self.player, self.song, self.level)
        self.spawn_update = min(max(50, self.song.rate - 50), 300)

        self.music_delay = 2000
        self.begin = False
        self.fov_update = True
        self.hit, self.miss = 0, 0
        self.monster_cool_down = 0
        self.score = 0
        self.energy = 1.0
        self.loop()

        self._bak.blit(0, 0, self._bak.width, self._bak.height, self.root, 0, 0)
        self.root.flush()

        self.player.done = False
        self.player.hp = self.player.max_hp
        player.x, player.y = exit_x, exit_y

    def attack(self, target):
        #self.player.cool_down = 100 # XXX: Weapon speed!

        damage = self.player.attack(target)
        if damage:
            damage *= self.energy
            target.hp -= damage
            text = "%s hits %s for %d damage." % (self.player.name, target.name, damage)
            if target.alive:
                self.score += damage
            else:
                text += "  The %s dies." % target.name
                self.score += target.y
                target.kill()
            self.status.add(text, True)
        else:
            self.status.add("%s misses." % self.player.name)

    def update(self, movement):
        # Tick
        dt = int(libtcod.sys_get_last_frame_length() * 1000)
        self.player.cool_down = max(0, self.player.cool_down - dt)

        # Erase all monsters and player
        for col in self.stage.monsters:
            for monster in self.stage.monsters[col]:
                self.stage.erase(monster)
        self.stage.erase(self.player)

        # Chomper kick back
        if self.player.y == self.chomper_y:
            self.player.y -= 1
            # Don't kick players back into monsters
            if not (self.player.x % 2):
                self.player.x += 1 if random.randrange(2) else -1

        # Handle player movement
        if movement:
            new_x = self.player.x + movement[0]
            new_y = self.player.y + movement[1]

            free_action = True
            for monster in self.stage.monsters.get(new_x, []):
                if monster.alive and (monster.y == new_y):
                    self.attack(monster)
                    free_action = False
            if free_action and self.stage.map[new_y][new_x].passable:
                self.player.x, self.player.y = new_x, new_y
                self.player.cool_down = self.stage.map[new_y][new_x].speed
                self.fov_update = True

        # Move monsters & push player
        push = (self.player.x, self.player.y)
        self.monster_cool_down -= dt
        if self.monster_cool_down <= 0:
            self.monster_cool_down = self.song.rate / 2
            for col in self.stage.monsters:
                for monster in self.stage.monsters[col]:
                    monster.y += 1
                    # Dead monsters don't push people :)
                    if monster.alive and ((monster.x, monster.y) == push):
                        self.fov_update = True  # Can't be pushed off torch :)
                        self.player.y += 1

        # Update lights
        if self.fov_update:
            self.fov.compute(self.player.x, self.player.y)
            self.fov_update = False

        for y in xrange(self.stage.height):
            for x in xrange(self.stage.width):
                tile = self.stage.map[y][x]
                # This is stupid.  Borrowed from the town where it kind of makes sense and
                # then hacked... 
                if ((x, y) in self.fov) or (self.static_light[y][x]):
                    self.stage.con.set_back(x, y, tile.light, libtcod.BKGND_SET)
                else:
                    self.stage.con.set_back(x, y, tile.dark, libtcod.BKGND_SET)

        # Start the music
        if not self.song.playing:
            self.music_delay -= dt
            if (not self.song.done) and (self.music_delay <= 0):
                self.begin = True
                self.status.add("Here we go!")

        # Update spawner and spawn monster
        spawn_count = float(len(self.stage.spawners))
        note_scale = MAX_NOTES / spawn_count
        spawn = dict() if not self.begin else dict([(int(round(n / note_scale)), n) for n in self.song.play(dt)[0]])
        for ix, spawner in enumerate(self.stage.spawners):
            spawner.update(dt)

            if ix in spawn:
                new_monster = spawner.spawn(spawn[ix], self.spawn_update)
                # If two monsters are spawned from a single spawner in one tick, push the second moster
                # one square to the right.  One two notes per tick anyway... I hope.
                row = self.stage.monsters.get(new_monster.x, [])
                if len(row) and row[-1].y == new_monster.y:
                    new_monster.x += 1
                self.stage.monsters.setdefault(new_monster.x, []).append(new_monster)
            self.stage.put(spawner)

        # Check for chomper hit
        if self.player.y == self.chomper_y:
            damage = self.stage.chompers[self.player.x-1].attack(self.player)
            self.status.add("%s chomped for %d points of damage." % (self.player.name, damage), unique=True)
            self.player.hp -= damage
            if self.player.hp <= 0:
                self.status.add("%s is done for the night." % self.player.name)
                self.player.done = "Chomped"
        for col in self.stage.monsters:
            for monster in self.stage.monsters[col]:
                if monster.y == self.chomper_y:
                    self.stage.chompers[monster.x-1].attack(monster)
                    if monster.alive:
                        self.status.add("A %s got through!" % monster.name)
                        self.jukebox.play(monster.note, DEBUG)
                        self.miss += 1
                        if not DEBUG:
                            self.energy -= (self.energy * 0.1)
                    else:
                        self.score += monster.max_hp
                        self.jukebox.play(monster.note, True)
                        self.hit += 1
                        self.energy += min(2.0, (2.0 - self.energy) * 0.1)

        for col in self.stage.monsters:
            self.stage.monsters[col] = [m for m in self.stage.monsters[col] if m.y < self.chomper_y]

        # Spin Chompers
        self.chomp_update -= dt
        if self.chomp_update <= 0:
            self.chomp_update = CHOMP_UPDATE
            for chomper in self.stage.chompers:
                chomper.update()
                self.stage.put(chomper)

        # Redraw monsters and player
        remaining = 0
        for col in self.stage.monsters:
            for monster in self.stage.monsters[col]:
                remaining += 1
                dead = not monster.alive
                x, y = monster.x, monster.y
                if ((x, y) in self.fov) or (self.static_light[y][x]) or dead:
                    self.stage.put(monster)
        self.stage.put(self.player)

        # Victory?
        if self.song.done and (not remaining) and (not self.jukebox.busy):
            self.player.done = "Win"
        elif not remaining:
            self.jukebox.fadeout(3000)
        elif self.energy < MIN_ENERGY:
            self.player.done = 'Fail'

        # Update all windows and blit into root
        self.effects.update(self.energy)
        self.status.update(self.root, self.player, self.score, self.energy)
        self.stage.update(self.root)

        if DEBUG: self.root.print_fps()
        self.root.flush()

        if self.player.done:
            self.active = False

    def loop(self):
        self.active = True
        self.stage.draw()
        libtcod.console_set_keyboard_repeat(50,5)
        while self.active and self.root.open:
            movement = actions.handle_keys(self.root, self.player, self.jukebox)
            self.update(movement)

        libtcod.console_set_keyboard_repeat(200, 30)
        self.effects.pause = True
        self.effects.update(self.energy)
        self.root.flush()
        results.report(self.root, self.player, self.score, self.energy, self.song, self.level, 
                       self.hit, self.miss, self.jukebox)
        self.effects.pause = False

def get_song(root, songs):
    selector = utils.ItemSelector(songs, title='Select a song to play:')
    song = selector.show(root)
  
    level = None
    if song is not None:
        width = 25
        levels = ['easy', 'medium', 'hard', 'expert']
        selector = menu.Menu(width, 8, 'Select a difficulty:', levels)
        level = selector.show(root, (root.width - width) / 2, 3, background_alpha=0.66)
        #selector = utils.ItemSelector(levels, title='Select a rating')
        #level = selector.show(root)
        if level is not None:
            level = levels.index(level) + 1
        else:
            song = None

    return song, level

